Skip to content

四、ESP32 UART如何控制LED灯?

1、流程

  1. 配置GPIO结构体,并使能GPIO(LED灯的引脚)
    1. gpio_config_t
    2. gpio_config(&gpio_config_t)
  2. 安装驱动程序 - 为 UART 驱动程序分配 ESP32-S3 资源
  3. 设置通信参数 - 设置波特率、数据位、停止位等
  4. 设置通信管脚 - 分配连接设备的管脚
  5. 运行 UART 通信 - 发送/接收数据
  6. 检测 if MSB 第1位数据是0还是1,并输出LED灯引脚高低电平,进而控制LED灯亮灭
    1. if (data[0] == '1')
    2. if (data[0] == '0')

五、什么是UART中断(FreeRTOS多任务+中断)?

5.1 先了解FreeRTOS是什么

乐鑫官方网页ESP-IDF FreeRTOS介绍:https://documentation.espressif.com/projects/esp-idf/zh_CN/latest/esp32s3/api-reference/system/freertos.html#freertos

❗ Important

因为ESP-IDF是基于C语言和FreeRTOS进行开发的,所以ESP-IDF与STM32HAL库区别很大,ESP-IDF的void app_main(void)就是FreeRTOS的主任务程序。

人眼眨眼频率相比于CPU频率

高铁并行

串行与并行以及并发概念图

5.2 为什么说ESP-IDF FreeRTOS任务的有4种状态?

IDF FreeRTOS 中任务的结构与 Vanilla FreeRTOS 相同。具体而言,IDF FreeRTOS 任务:

  • 只能处于以下任一状态:运行中、就绪、阻塞或挂起。
  • 任务函数通常为无限循环。
  • 任务函数不应返回。

5.3 ESP-IDF FreeRTOS有哪些API参考?

https://documentation.espressif.com/projects/esp-idf/zh_CN/latest/esp32s3/api-reference/system/freertos_idf.html#id22

UART FreeRTOS 引入

static inline BaseType_txTaskCreate(TaskFunction_t pxTaskCode, const char *const pcName, const configSTACK_DEPTH_TYPE usStackDepth, void *const pvParameters, UBaseType_t uxPriority, TaskHandle_t *const pxCreatedTask)

示例代码:
plain
// Task to be created.
void vTaskCode( void * pvParameters )
{
for( ;; )
  {
// Task code goes here.
  }
}

// Function that creates a task.
void vOtherFunction( void )
{
static uint8_t ucParameterToPass;
TaskHandle_t xHandle = NULL;

// Create the task, storing the handle.  Note that the passed parameter ucParameterToPass
// must exist for the lifetime of the task, so in this case is declared static.  If it was just an
// an automatic stack variable it might no longer exist, or at least have been corrupted, by the time
// the new task attempts to access it.
  xTaskCreate( vTaskCode, "NAME", STACK_SIZE, &ucParameterToPass, tskIDLE_PRIORITY, &xHandle );
  configASSERT( xHandle );

// Use the handle to delete the task.
if( xHandle != NULL )
  {
     vTaskDelete( xHandle );
  }
}

5.4 如何实现UART与FreeRTOS结合的项目目标?

  1. 绿色的灯每间隔100毫秒(Ms)闪烁一次,要求无限循环闪烁
  2. 红色的LED灯每间隔1.5秒(s)闪烁一次,也同样要求无限循环闪烁
plain
void app_main(void)
{
	 while(1){
		gpio_set_level(GPIO_NUM_2, 1);	// 开灯
		vTaskDelay(pdMS_TO_TICKS(100));
		gpio_set_level(GPIO_NUM_2, 0);	// 关灯
		vTaskDelay(pdMS_TO_TICKS(100));	// 100毫秒

		gpio_set_level(GPIO_NUM_4, 1);	// 开灯
		vTaskDelay(pdMS_TO_TICKS(1500));
		gpio_set_level(GPIO_NUM_4, 0);	// 关灯
		vTaskDelay(pdMS_TO_TICKS(1500));	// 1.5秒		
	}
}

5.4.1 代码实现UART与FreeRTOS结合

plain
#include <stdio.h>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"


void led_green_2(void *pvParameters){

    for(;;){
        gpio_set_level(GPIO_NUM_2, 1);	// 开灯
		vTaskDelay(pdMS_TO_TICKS(100));
		gpio_set_level(GPIO_NUM_2, 0);	// 关灯
		vTaskDelay(pdMS_TO_TICKS(100));	// 100毫秒
    }
}


void led_red_4(void * pvParameters){
    for(;;){
        gpio_set_level(GPIO_NUM_4, 1);	// 开灯
		vTaskDelay(pdMS_TO_TICKS(1500));
		gpio_set_level(GPIO_NUM_4, 0);	// 关灯
		vTaskDelay(pdMS_TO_TICKS(1500));	// 1.5秒
    }
}


void app_main(void)
{
    // 一、配置GPIO结构,再使能GPIO
    gpio_config_t my_gpio_config = {
        .pin_bit_mask = (1ULL << GPIO_NUM_2) | (1ULL << GPIO_NUM_4),
        .mode = GPIO_MODE_OUTPUT,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .intr_type = GPIO_INTR_DISABLE,
    };

    gpio_config(&my_gpio_config);

    

    // 二、创建FreeRTOS task,封装task任务函数,最后传参到xTaskCreate()

    xTaskCreate(led_green_2, "led_green_2", 2048, NULL, 0, NULL);
    xTaskCreate(led_red_4, "led_red_4", 1024, NULL, 0, NULL);

}

六、什么是UART中断(Queue队列+非阻塞等待任务触发)?

6.0 如何理解中断概念?

  1. 中断的概念
  2. 中断优先级的概念
  3. 中断处理机制

中断的概念(门主引擎)

中断优先级的概念(门主引擎)

中断处理机制(门主引擎)

中断处理机制(门主引擎)

中断机制漫画解读 中断优先级排序

中断机制漫画解读 中断优先级排序2

6.1 ESP-IDF框架下ESP32-S3中断与STM32中断比对

1. 不同单片机的中断处理流程对比:STM32 vs ESP32 对比图

1.1 STM32(裸机):
中断 → 直接进 ISR → 点亮 LED
✅ 简单直接

1.2 ESP32(FreeRTOS):

中断 → ISR → ??? → 点亮 LED
❌ ISR 里不能做复杂事!

2. 解释说明:

“在 ESP32 上,ISR(中断服务函数)运行在高危环境:

2.1 不能调用大多数 API(比如 printf、malloc)

2.2 不能访问 Flash(可能卡死)

2.3 必须快进快出(微秒级)

3. 所以,这就是为什么ESP-IDF官方禁止在 ISR 里直接处理业务逻辑

那怎么办?答案是:让 ISR 只发一个‘通知’,真正的处理交给后台任务!”

3.1 这里的ISR(中断服务函数)就类似于战争时期指挥部的通信兵,

3.2 首先侦察兵接收到前方突发敌袭、炮轰(也就是中断源触发了),

3.3 然后经过中断矩阵(侦察兵经过营地找通信兵),

3.4 通信兵接收到消息后应该要立刻将敌袭消息发送到队列,任务获取Queue队列的消息,执行任务

3.5 ISR接下来继续等待中断源通道(侦察兵)再次来消息即可

🚨 严重错误!

3.4.1 ISR必须响应极快,而不是先去拉泡屎、撒泡尿、吃个饭、撩下妹再来发送敌袭消息,延误了战机要拉出去执行军令状的。

ESP-IDF框架下ESP32-S3中断比对

关于 ISR 的图片 胡闹

6.2 中断前置知识(FreeRTO之Queue队列+多任务)

💡 Tip

  • 队列可以跨任务通信传递消息
章节编号章节名称核心内容(小白友好版)时长占比官方文档对标
0课前回顾 & 预告1. 回顾已讲:UART 轮询控制 LED(官方 1-4 步); 2. 抛出痛点:轮询延迟高、占 CPU; 3. 预告:用中断 + FreeRTOS 解决,今天学 2 个核心:队列 / 任务 + UART 中断2 分钟无(衔接内容)
1前置知识:FreeRTOS 队列 & 任务(仅讲和 UART 中断相关的)1. 队列(Queue): - 比喻:前台(中断)不能处理复杂工作,把消息丢进 “传达室信箱(队列)”,后台(任务)慢慢取; - 核心 API:xQueueReceive(取消息); - 关键参数:阻塞时间(portMAX_DELAY= 等不到消息不干活); 2. 任务(Task): - 比喻:专门 “处理信箱消息” 的后台员工; - 核心 API:xTaskCreate(雇一个后台员工); - 关键参数:优先级(员工优先级高于前台); 3. 中断 + 队列 + 任务的关系:画流程图(中断→队列→任务)5 分钟FreeRTOS 官方文档
2官方流程第 5 步:UART 中断(核心)按官方流程拆解,只讲 “使用中断” 的关键步骤: 1. 步骤 1:修改uart_driver_install(创建队列,官方第一步); 2. 步骤 2:配置 UART 中断阈值(1 字节触发); 3. 步骤 3:启用 UART 接收中断; 4. 步骤 4:创建事件处理任务(读取队列 + 控制 LED)8 分钟UART 中断官方文档
3代码实战 & 效果演示1. 逐行讲解修改后的代码(标注新增 / 修改); 2. 烧录测试: - 发 1→LED 立即亮(无 500ms 延迟); - 发 0→LED 立即灭; - 快速发 1010→LED 快速闪烁(对比轮询的卡顿); 3. 打印日志:主任务空闲(证明不占 CPU)4 分钟无(实战验证)
4避坑指南 & 答疑1. 常见坑: - 坑 1:队列没创建(uart_driver_install队列参数错)→ 解决:检查第 6 个参数; - 坑 2:中断阈值设太大→ 解决:设为 1; - 坑 3:任务栈太小→ 解决:设 4096; 2. 答疑:为什么中断标志位设 0?(官方默认配置)2 分钟UART 驱动安装 API 文档
5总结 & 拓展1. 核心总结:3 个关键点(队列传消息、任务处理、1 字节触发中断); 2. 拓展:下节课讲模式检测中断(识别 +++)1 分钟

FreeRTOS关于Queue队列前置知识

6.3 ESP-IDF的UART中断事件处理流程是怎么样的?

你配置 uart_intr_config_t

UART 硬件在满足条件时产生中断(如 RX 超时)

ESP-IDF 的 UART ISR 被调用

ISR 将硬件中断“翻译”成 uart_event_type_t(如 UART_DATA)

事件被放入你创建的 uart_event_queue

你的任务通过 xQueueReceive() 读取 uart_event_t 并处理

ESP-IDF的UART中断事件处理流程

6.4 中断ESP-IDF官方示例代码讲了什么?

plain
const uart_port_t uart_num = UART_NUM_2;
// Configure a UART interrupt threshold and timeout
uart_intr_config_t uart_intr = {
    .intr_enable_mask = UART_INTR_RXFIFO_FULL | UART_INTR_RXFIFO_TOUT,
    .rxfifo_full_thresh = 100,
    .rx_timeout_thresh = 10,
};
ESP_ERROR_CHECK(uart_intr_config(uart_num, &uart_intr));

// Enable UART RX FIFO full threshold and timeout interrupts
ESP_ERROR_CHECK(uart_enable_rx_intr(uart_num));

ESP32-S3 UART中断控制LED点亮流程示意图

6.5 UART中断流程代码步骤如何编写?

https://documentation.espressif.com/projects/esp-idf/zh_CN/latest/esp32s3/api-reference/system/freertos_idf.html#id24

  1. 创建Queue队列句柄,方便后期使用队列句柄传送中断消息给任务函数(中断 + 队列 + 任务的关系:(中断→队列→任务))
  2. 配置GPIO结构体,并使能GPIO(2颗LED灯)
  3. 配置UART及UART中断
    1. 安装驱动程序 - 为 UART 驱动程序分配 ESP32-S3 资源
      1. 步骤 1:修改uart_driver_install(创建队列,官方第一步);
    2. 设置通信参数 - 设置波特率、数据位、停止位等
    3. 设置通信管脚 - 分配连接设备的管脚
    4. 运行 UART 通信 - 发送/接收数据
    5. 使用中断 - 触发特定通信事件的中断
      1. 步骤 2:配置 UART 中断阈值(1 字节触发);
      2. 步骤 3:启用 UART 接收中断;
  4. 创建事件处理任务(读取队列 + 控制 LED)
    1. 创建FreeRTO Task任务,
    2. 创建Task任务的同时,编写并封装好中断服务函数
    3. Task任务①为GPIO2号绿色LED灯每隔100毫秒闪烁一次
    4. Task任务②为GPIO4号红色LED灯被UART中断控制(中断触发→队列传送中断被触发的消息→Task任务②唤醒并执行任务)

UART中断流程代码编写步骤

6.6 代码实现UART中断(Queue队列+非阻塞等待任务触发)

plain
#include <stdio.h>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "hal/uart_hal.h"
#include "esp_log.h"

// 新增:队列句柄(中断事件的“中转站”)
QueueHandle_t my_uart_intr_queue_handle;

void my_led_initialize(void){
    gpio_config_t my_gpio_config = {
        .pin_bit_mask = (1ULL << GPIO_NUM_2) | (1ULL << GPIO_NUM_4),
        .mode = GPIO_MODE_OUTPUT,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .intr_type = GPIO_INTR_DISABLE,
    };
    gpio_config(&my_gpio_config);
    gpio_set_level(GPIO_NUM_4, 0);
}


void led_green_2(void *pvParameters){

    for(;;){
        gpio_set_level(GPIO_NUM_2, 1);	// 开灯
		vTaskDelay(pdMS_TO_TICKS(100));
		gpio_set_level(GPIO_NUM_2, 0);	// 关灯
		vTaskDelay(pdMS_TO_TICKS(100));	// 100毫秒
    }
}


void led_red_4(void * pvParameters)
{   uart_event_t my_uart_intr_event_type;
    uint8_t rx_buffer_zone[128];
    for(;;)
    {
        if(xQueueReceive(my_uart_intr_queue_handle, &my_uart_intr_event_type, portMAX_DELAY)){
            if( my_uart_intr_event_type.type == UART_DATA && my_uart_intr_event_type.size > 0){
                ESP_LOGI("uart_intr_trigger", "bytes length: %d", my_uart_intr_event_type.size);

                uart_read_bytes(UART_NUM_0, rx_buffer_zone, my_uart_intr_event_type.size, pdMS_TO_TICKS(100));

                if (my_uart_intr_event_type.size >= 6 &&  memcmp(rx_buffer_zone, "led_on", 6) == 0 ){
                    gpio_set_level(GPIO_NUM_4, 1);
                    ESP_LOGI("Received CMD", "LED ON!");
                }

                if (my_uart_intr_event_type.size >= 7 && memcmp(rx_buffer_zone, "led_off", 7) == 0 ){
                    gpio_set_level(GPIO_NUM_4, 0);
                    ESP_LOGI("Received CMD", "LED OFF!");
                }

            }

        }
        
    }
}


void app_main(void)
{   /*一、配置GPIO结构,再使能GPIO*/
    my_led_initialize();

    /*二、注册UART及启动中断,配置UART结构体parameters并使能,配置UART RX以及TX引脚*/
    uart_config_t my_uart_tele_config = {
        .baud_rate = 115200,
        .data_bits = UART_DATA_8_BITS,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
        .parity = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .source_clk = UART_SCLK_DEFAULT,
    };

    uart_driver_install(UART_NUM_0, 1024, 0, 2, &my_uart_intr_queue_handle, 0);
    uart_param_config(UART_NUM_0, &my_uart_tele_config);
    uart_set_pin(UART_NUM_0, GPIO_NUM_43, GPIO_NUM_44, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);

    /*三、显示配置UART中断参数并启用uart intr config() */
    uart_intr_config_t my_uart_intr_config = {
        .intr_enable_mask = UART_INTR_RXFIFO_FULL | UART_INTR_RXFIFO_TOUT,
        .rx_timeout_thresh = 10,
        .rxfifo_full_thresh = 64,
        .txfifo_empty_intr_thresh = 0,

    };
    ESP_ERROR_CHECK( uart_intr_config(UART_NUM_0, &my_uart_intr_config) );
    /* 使能rx环形缓冲区中断 */
    ESP_ERROR_CHECK( uart_enable_rx_intr(UART_NUM_0) );

    /*四、创建FreeRTOS task,封装task任务函数,最后传参到xTaskCreate()*/
    xTaskCreate(led_green_2, "led_green_2", 1024, NULL, 0, NULL);
    xTaskCreate(led_red_4, "led_red_4", 2048, NULL, 0, NULL);
}
plain
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "string.h"
#include "esp_log.h"

// 你UART的引脚定义
#define tx_pin GPIO_NUM_43
#define rx_pin GPIO_NUM_44
// 新增:日志标签(方便调试)
#define TAG "UART_INTERRUPT"
// 新增:队列句柄(中断事件的“中转站”)
QueueHandle_t uart_event_queue;

// LED GPIO初始化
void led_init(void) {
    gpio_config_t my_led_gpio_config = {0};
    my_led_gpio_config.pin_bit_mask = 1ULL << GPIO_NUM_2;
    my_led_gpio_config.mode = GPIO_MODE_OUTPUT;
    my_led_gpio_config.pull_down_en = GPIO_PULLDOWN_DISABLE;
    my_led_gpio_config.pull_up_en = GPIO_PULLUP_DISABLE;
    my_led_gpio_config.intr_type = GPIO_INTR_DISABLE;
    gpio_config(&my_led_gpio_config);
    // 新增:初始熄灭LED
    gpio_set_level(GPIO_NUM_2, 0);
}

// 新增:UART事件处理任务(专门处理中断事件,FreeRTOS核心)
void uart_event_task(void *arg) {
    uart_event_t event;  // UART事件结构体(SDK定义)
    size_t buffered_len;
    uint8_t data[128] = {0};  

    // 死循环:一直等待队列中的中断事件(理解:“一直看短信”)
    for(;;) {
        // 从队列读取事件:portMAX_DELAY=永久阻塞,直到有事件
        if (xQueueReceive(uart_event_queue, &event, portMAX_DELAY)) {
            switch (event.type) {
                // 事件1:收到UART数据(中断触发)
                case UART_DATA:
                    // 获取缓冲区中的数据长度(替代你原来的uart_get_buffered_data_len)
                    uart_get_buffered_data_len(UART_NUM_0, &buffered_len);
                    // 读取数据,UART核心控制器读取接收数据缓冲区数据
                    int rxBytes = uart_read_bytes(UART_NUM_0, data, buffered_len, 100);
                    if (rxBytes > 0) {
                        data[rxBytes] = 0;  // 字符串结束符
                        ESP_LOGI(TAG, "收到数据:%s", data);

                        // 原来的LED控制逻辑
                        if (data[0] == '1') {
                            gpio_set_level(GPIO_NUM_2, 1);
                        }
                        if (data[0] == '0') {
                            gpio_set_level(GPIO_NUM_2, 0);
                        }
                    }
                    break;

                // 事件2:FIFO溢出(避坑:数据太多没及时处理)
                case UART_FIFO_OVF:
                    ESP_LOGE(TAG, "警告:UART FIFO溢出!");
                    uart_flush(UART_NUM_0);  // 清空缓冲区
                    break;

                // 其他事件(可暂时忽略,留拓展空间)
                default:
                    ESP_LOGW(TAG, "未处理的UART事件:%d", event.type);
                    break;
            }
        }
    }
    // 任务删除(实际不会执行到)
    vTaskDelete(NULL);
}

void app_main(void) {
    // 保留:LED初始化
    led_init();

    // UART配置结构体(仅新增注释,内容不变)
    uart_config_t my_uart_config = {0};
    my_uart_config.baud_rate = 115200;
    my_uart_config.data_bits = UART_DATA_8_BITS;
    my_uart_config.parity = UART_PARITY_DISABLE;
    my_uart_config.stop_bits = UART_STOP_BITS_1;
    my_uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
    my_uart_config.source_clk = UART_SCLK_DEFAULT;

    // 修改:uart_driver_install(新增队列参数,核心改动!)
    // 原代码:uart_driver_install(UART_NUM_0, 1024, 0, 0, NULL, 0);
    // 新代码:最后4个参数:队列深度=5,队列句柄=uart_event_queue,中断标志=0
    uart_driver_install(UART_NUM_0, 1024, 0, 5, &uart_event_queue, 0);

    // UART参数配置
    uart_param_config(UART_NUM_0, &my_uart_config);
    // UART引脚配置
    uart_set_pin(UART_NUM_0, tx_pin, rx_pin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);

    // 新增:UART中断配置(重点:设置1字节触发中断)
    uart_intr_config_t uart_intr = {0};
    uart_intr.intr_enable_mask = UART_INTR_RXFIFO_FULL | UART_INTR_RXFIFO_TOUT;
    uart_intr.rxfifo_full_thresh = 1;  // 收到1个字节就触发中断(无延迟)
    uart_intr.rx_timeout_thresh = 10;  // 超时阈值(防止漏数据)
    uart_intr_config(UART_NUM_0, &uart_intr);

    // 新增:启用UART接收中断(SDK自动处理底层中断矩阵)
    uart_enable_rx_intr(UART_NUM_0);

    // 新增:创建FreeRTOS任务(处理UART事件)
    // 参数:任务函数、任务名、栈大小、参数、优先级、任务句柄
    xTaskCreate(
        uart_event_task,   // 任务函数(上面定义的)
        "uart_event_task", // 任务名(给任务起个名字)
        4096,              // 栈大小(4096足够,不用改)
        NULL,              // 任务参数(无)
        10,                // 优先级(高于主任务的默认优先级1)
        NULL               // 任务句柄(不用保存)
    );

    // 修改:主循环(去掉轮询逻辑,改为“闲任务”)
    // 原代码:while(1)轮询读取UART数据
    // 新代码:主任务只打印日志,证明不占CPU
    while (1) {
        ESP_LOGI(TAG, "主任务运行中(CPU空闲)...");
        vTaskDelay(pdMS_TO_TICKS(1000));  // 1秒打印一次
    }
}

觉醒,然后燎原。 © 2026 门主引擎